package model;

import java.util.Vector;

import lock.ConcreteLock;
import lock.Lock;

public class PTOMonitor {
	private static PTOMonitor instance;

	public static synchronized PTOMonitor getInstance(){
		if(instance == null){
			instance = new PTOMonitor();
		}
		return instance;
	}
	private final Vector<DiningPermissionToken> tokens;
	private final Vector<Philosopher> philosophers;
	private final Vector<Observer> observers;
	private final Lock notifyStateChangelock = new ConcreteLock(false);
	private int maxThinkingTime = 1000;
	private int maxEatingTime = 1000;
	private int maxEatingPhilosophers;
	private int eatingPhilosophers;

	private int overallEatingPhilosophers;
	private int stateChanges;
	private double  averageEatingPhilosophers;
	private int amountOfPhilosophers;
	private int ptoViolations;

	public int getPTOViolations() {
		return this.ptoViolations;
	}

	public void setPTOViolations(final int ptoViolations) {
		this.ptoViolations = ptoViolations;
	}
	
	public int getStateChanges() {
		return stateChanges;
	}

	public int getMaxThinkingTime() {
		return maxThinkingTime;
	}

	public int getMaxEatingTime() {
		return maxEatingTime;
	}
	
	public void setMaxThinkingTime(final int maxThinkingTime) {
		this.maxThinkingTime = maxThinkingTime;
	}

	public void setMaxEatingTime(final int maxEatingTime) {
		this.maxEatingTime = maxEatingTime;
	}
	
	public int getEatingPhilosophers() {
		return eatingPhilosophers;
	}

	private int getCurrentlyEatingPhilosophers() {
		return eatingPhilosophers;
	}
	
	public double getAverageEatingPhilosophers() {
		return this.averageEatingPhilosophers;
	}
	
	private PTOMonitor() {
		this.tokens = new Vector<DiningPermissionToken>();
		this.philosophers = new Vector<Philosopher>();
		this.observers = new Vector<Observer>();
	}

	/**
	 * Sets the currently eating {@link Philosopher}s.
	 * If the new amount is not the old amount the following values will be calculated anew:
	 * The overall eating philosophers.
	 * The average eating philosophers.
	 * (The max amount of eating philosophers).
	 * @param eatingPhilosophers is the new amount of currently eating {@link Philosopher}s.
	 */
	private void setCurrentlyEatingPhilosophers(final int eatingPhilosophers) {
		if(this.eatingPhilosophers != eatingPhilosophers){
			this.overallEatingPhilosophers += eatingPhilosophers;
			this.eatingPhilosophers = eatingPhilosophers;
			this.averageEatingPhilosophers = (this.overallEatingPhilosophers / this.stateChanges);
			if(this.eatingPhilosophers > this.maxEatingPhilosophers){
				this.maxEatingPhilosophers = this.eatingPhilosophers;
			}
		}
	}
	/**
	 * Notifies the {@link PTOMonitor} that the state of a {@link Philosopher} has changed.
	 * Increases the amount of state changes by one.
	 * If the new state is {@link Eating}, checks if this state change violated the Philosopher table order.
	 * Increases the amount of PTO violations if the state change violated the PTO.
	 * Also notifies all observers.
	 * @param state is the new {@link PhilosopherState} of the provided {@link Philosopher}.
	 * @param philosopher is the {@link Philosopher} belonging to the state.
	 */
	public void notifyStateChange(final PhilosopherState state, final Philosopher philosopher) {
		this.notifyStateChangelock.lock();
		this.stateChanges++;
		state.accept(new PhilosopherStateVisitor() {
			@Override
			public void visit(final Thinking thinking) {
				PTOMonitor.this.setCurrentlyEatingPhilosophers(PTOMonitor.this.getCurrentlyEatingPhilosophers() - 1);
			}
			
			@Override
			public void visit(final Eating eating) {
				PTOMonitor.this.setCurrentlyEatingPhilosophers(PTOMonitor.this.getCurrentlyEatingPhilosophers() + 1);
				final int currentPhilosopher = philosopher.getNumber();
				final int philosopherCount = PTOMonitor.this.amountOfPhilosophers;
				final Philosopher leftPhilosopher = PTOMonitor.this.getPhilosophers().get(currentPhilosopher == 0 ? philosopherCount - 1: currentPhilosopher - 1);
				final Philosopher rightPhilosopher = PTOMonitor.this.getPhilosophers().get((currentPhilosopher + 1) % philosopherCount);
				if(philosopherCount > 1){
					if(leftPhilosopher.isEating() ||
						(philosopherCount > 2 && rightPhilosopher.isEating())){
						PTOMonitor.this.setPTOViolations(PTOMonitor.this.getPTOViolations() + 1);
						System.out.println(leftPhilosopher + " "+ philosopher + " " + rightPhilosopher);
					}
				}
			}

			@Override
			public void visit(final LeftTokenObtained leftTokenObtained) {}

			@Override
			public void visit(final StopEating stopEating) {}
		});
		this.notifyObservers();
		this.notifyStateChangelock.unlock();
	}
	
	protected Vector<Philosopher> getPhilosophers(){
		return this.philosophers;
	}

	/**
	 * Creates as many {@link Philosopher} as provided.
	 * @param count is the amount of {@link Philosopher} that should be created and started.
	 * @param useDefaultProtocol is whether the default protocol should be used over the {@link CounterClockwiseProtocol}.
	 * @throws InvalidAmountException if count is less than 2
	 */ 
	public void start(final int count, final boolean useDefaultProtocol) throws InvalidAmountException{
		if(count < 2) throw new InvalidAmountException();
		for (int i = 0; i < count; i++) {
			this.tokens.add(new DiningPermissionToken());
		}
		for (int i = 0; i < count; i++) {			
			this.philosophers.add(this.createPhilosopher(useDefaultProtocol));
		}
		for (final Philosopher philosopher : this.philosophers) {
			new Thread(philosopher,"Thread" + philosopher.getNumber()).start();
		}
	}

	/**
	 * Creates a single {@link Philosopher}.
	 * @param useDefaultProtocol is whether the default protocol should be used over the {@link CounterClockwiseProtocol}.
	 * @return a new {@link Philosopher}.
	 */
	private Philosopher createPhilosopher(final boolean useDefaultProtocol){
		final Philosopher result;
		final int number = this.amountOfPhilosophers++;
		if(useDefaultProtocol){
			result = new Philosopher(number, new DefaultPhilosopherProtocol());
		}else {
			final DiningPermissionToken leftToken = this.tokens.get(number);
			final DiningPermissionToken rightToken = this.tokens.get((number + 1) % this.tokens.size());;
			result = new Philosopher(number, new CounterClockwiseProtocol(leftToken,rightToken));
		}
		return result;
	}

	/**
	 * Stops all running {@link Philosopher}s.
	 */
	public void stop() {
		for (final Philosopher philosopher : this.philosophers) {
			philosopher.stop();
		}
	}

	public void register(final Observer observer){
		this.observers.add(observer);
	}
	
	public void deregister(final Object observer){
		this.observers.remove(observer);
	}
	
	public void notifyObservers(){
		for (final Observer observer : this.observers) {
			observer.update();
		}
	}

	public void printStatistics() {
		this.notifyStateChangelock.lock();
		final String s = this.stateChanges == 0 ? "?" : String.valueOf((double)this.overallEatingPhilosophers / this.stateChanges);
		System.out.println("Currently: " + this.eatingPhilosophers + 
						   " MAX: " + this.maxEatingPhilosophers + 
						   " AVG: " + this.overallEatingPhilosophers + "/" + this.stateChanges + " ("+ s + ")");
		this.notifyStateChangelock.unlock();
	}


}